﻿namespace org.webguvenligi.NSecureImageLib
{
    #region Imported Libraries
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.IO;
    using System.Linq;
    using System.Drawing.Imaging;
    using org.webguvenligi.NSecureImageLib.GraphicFileFormats.Bitmap;
    using org.webguvenligi.NSecureImageLib.GraphicFileFormats.Utilities;
    using org.webguvenligi.NSecureImageLib.GraphicFileFormats.PNG;
    #endregion
    //TODO Add Support For More Image Types
    //TODO Add Support For Parsing Image Formats Without Using .Net Api To Protect Against GDI+ Overflow or Bugs
    //TODO Write More Unit Tests :)
    //TODO Port secureImage.php libraries image fixing algorithm here
    public static class NSecureImage
    {
        private const string errorMessage = "Could not recognise image format.";

        public static bool IsValidImage(string uploadDirectory, string imageName, ImageFormat exceptedImageFormat)
        {
            ImageFormat foundImageFormat =
                IsValidImage(uploadDirectory, imageName);
            return (foundImageFormat.Guid == exceptedImageFormat.Guid);
        }
        /// <summary>
        /// Checks The Given Image In The Given Path For Validity
        /// </summary>
        /// <param name="uploadDirectory">The Directory In Which The File Resides</param>
        /// <param name="imageName">The Name Of The Image File</param>
        /// <exception></exception>
        /// <returns></returns>
        public static ImageFormat IsValidImage(string uploadDirectory, string imageName)
        {

            if (!System.IO.Directory.Exists(uploadDirectory))
                throw new NSecureImageException("File System Exception Has Occurred", new DirectoryNotFoundException(uploadDirectory));

            string targetFile =
                System.IO.Path.Combine(uploadDirectory, imageName);

            if (!System.IO.File.Exists(targetFile))
                throw new NSecureImageException("File System Exception Has Occurred", new FileNotFoundException(targetFile));
            try
            {
                bool checkedImageFast =
                    GetDimensions(targetFile);
                if (!checkedImageFast)
                    throw new NSecureImageException("Basic Image Magic Number Detection Failed.");
            }
            catch (Exception ex)
            {
                throw new NSecureImageException("Basic Image Magic Number Detection Failed.", ex);
            }
            try
            {
                Image targetImage = Image.FromFile(targetFile);
                return targetImage.RawFormat;
            }
            catch (Exception externalException)
            {
                throw new NSecureImageException("GDI Based Parsing Exception Occurred. Possible Crafted Image File.", externalException);
            }
        }

        private static Dictionary<byte[], Func<BinaryReader, bool>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, bool>>()
        {
            { new byte[]{ 0x42, 0x4D }, DecodeBitmap},
            { new byte[]{ 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },
            { new byte[]{ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },
            { new byte[]{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
            { new byte[]{ 0xff, 0xd8 }, DecodeJfif },
        };

        private static bool GetDimensions(Stream imageStream)
        {
            using (BinaryReader binaryReader = new BinaryReader(imageStream))
            {
                try
                {
                    return GetDimensions(binaryReader);
                }
                catch (ArgumentException e)
                {
                    if (e.Message.StartsWith(errorMessage))
                    {
                        throw new ArgumentException(errorMessage, "path", e);
                    }
                    else
                    {
                        throw;
                    }
                }
            }
        }
        /// <summary>
        /// Gets the dimensions of an image.
        /// </summary>
        /// <param name="path">The path of the image to get the dimensions of.</param>
        /// <returns>The dimensions of the specified image.</returns>
        /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>
        private static bool GetDimensions(string path)
        {
            return GetDimensions(File.OpenRead(path));
        }

        /// &lt;summary&gt;
        /// Gets the dimensions of an image.
        /// &lt;/summary&gt;
        /// &lt;param name="path"&gt;The path of the image to get the dimensions of.&lt;/param&gt;
        /// &lt;returns&gt;The dimensions of the specified image.&lt;/returns&gt;
        /// &lt;exception cref="ArgumentException"&gt;The image was of an unrecognised format.&lt;/exception&gt;    
        private static bool GetDimensions(BinaryReader binaryReader)
        {
            int maxMagicBytesLength = 
                imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;

            byte[] magicBytes = new byte[maxMagicBytesLength];

            for (int i = 0; i < maxMagicBytesLength; i += 1)
            {
                magicBytes[i] = binaryReader.ReadByte();

                foreach (var kvPair in imageFormatDecoders)
                {
                    if (magicBytes.StartsWith(kvPair.Key))
                    {
                        return kvPair.Value(binaryReader);
                    }
                }
            }

            throw new ArgumentException(errorMessage, "binaryReader");
        }

        private static bool DecodeBitmap(BinaryReader binaryReader)
        {
            try
            {
                using (BitmapParser tempParser =
                    new BitmapParser(binaryReader.BaseStream))
                {
                    return true;
                    //return new Size((int)tempParser.InfoHeader.Width, (int)tempParser.InfoHeader.Height);
                }
            }
            catch (Exception ex)
            {
                throw new NSecureImageException("Bitmap Header Wrong", ex);
            }
        }

        private static bool DecodeGif(BinaryReader binaryReader)
        {
            try
            {
                int width = binaryReader.ReadInt16();
                int height = binaryReader.ReadInt16();
                //return new Size(width, height);
                return true;
            }
            catch (Exception ex)
            {
                throw new NSecureImageException("Gif Header Wrong", ex);
            }
        }

        private static bool DecodePng(BinaryReader binaryReader)
        {
            try
            {
                using (PNGParser tempParser = new PNGParser(binaryReader.BaseStream))
                {
                    tempParser.CleanAdditionalChunks();
                    return true;
                }
            }
            catch (Exception ex)
            {
                throw new NSecureImageException("Png Header Wrong", ex);
            }            
        }

        private static bool DecodeJfif(BinaryReader binaryReader)
        {
            try
            {
                while (binaryReader.ReadByte() == 0xff)
                {
                    byte marker = binaryReader.ReadByte();
                    short chunkLength = binaryReader.ReadLittleEndianInt16();
                    if (marker == 0xc0)
                    {
                        binaryReader.ReadByte();

                        int height = binaryReader.ReadLittleEndianInt16();
                        int width = binaryReader.ReadLittleEndianInt16();
                        return true;
                    }
                    binaryReader.ReadBytes(chunkLength - 2);
                }
            }
            catch (Exception ex)
            {
                throw new NSecureImageException("Jpeg Header Wrong", ex);
            }
            throw new NSecureImageException("Jpeg Header Wrong", new ArgumentException(errorMessage));
        }
    }
}